]>
Commit | Line | Data |
---|---|---|
5e8ff485 RBR |
1 | import Cocoa |
2 | import SwiftUI | |
3 | ||
4 | class MapTextEditorController: NSViewController { | |
5 | ||
6 | @Binding var text: String | |
fdb4633d | 7 | let onChange: () -> Void |
5e8ff485 | 8 | |
77d0155b RBR |
9 | private let vertexRegex = MapParsingPatterns.vertex |
10 | private let edgeRegex = MapParsingPatterns.edge | |
11 | private let blockerRegex = MapParsingPatterns.blocker | |
12 | private let opportunityRegex = MapParsingPatterns.opportunity | |
fdb4633d | 13 | private let noteRegex = MapParsingPatterns.note |
77d0155b RBR |
14 | private let stageRegex = MapParsingPatterns.stage |
15 | ||
fdb4633d | 16 | private let changeDebouncer: Debouncer = Debouncer(seconds: 1) |
77d0155b | 17 | |
fdb4633d | 18 | init(text: Binding<String>, onChange: @escaping () -> Void) { |
5e8ff485 | 19 | self._text = text |
fdb4633d | 20 | self.onChange = onChange |
5e8ff485 RBR |
21 | super.init(nibName: nil, bundle: nil) |
22 | } | |
23 | ||
24 | required init?(coder: NSCoder) { | |
25 | fatalError("init(coder:) has not been implemented") | |
26 | } | |
27 | ||
28 | override func loadView() { | |
29 | let scrollView = NSTextView.scrollableTextView() | |
30 | let textView = scrollView.documentView as! NSTextView | |
31 | ||
32 | scrollView.translatesAutoresizingMaskIntoConstraints = false | |
33 | ||
75a0e450 | 34 | textView.allowsUndo = true |
5e8ff485 | 35 | textView.delegate = self |
77d0155b | 36 | textView.textStorage?.delegate = self |
5e8ff485 RBR |
37 | textView.string = self.text |
38 | textView.isEditable = true | |
39 | textView.font = .monospacedSystemFont(ofSize: 16.0, weight: .regular) | |
40 | self.view = scrollView | |
41 | } | |
42 | ||
43 | override func viewDidAppear() { | |
44 | self.view.window?.makeFirstResponder(self.view) | |
45 | } | |
46 | } | |
47 | ||
48 | extension MapTextEditorController: NSTextViewDelegate { | |
49 | ||
50 | func textDidChange(_ obj: Notification) { | |
51 | if let textField = obj.object as? NSTextView { | |
52 | self.text = textField.string | |
fdb4633d RBR |
53 | |
54 | ||
55 | changeDebouncer.debounce { | |
56 | DispatchQueue.main.async { | |
57 | self.onChange() | |
58 | } | |
59 | } | |
5e8ff485 RBR |
60 | } |
61 | } | |
62 | ||
63 | func textView(_ view: NSTextView, shouldChangeTextIn: NSRange, replacementString: String?) -> Bool | |
64 | { | |
65 | let range = Range(shouldChangeTextIn, in: view.string) | |
66 | let target = view.string[range!] | |
67 | ||
68 | if target == "--" { | |
69 | return false | |
70 | } | |
71 | ||
72 | return true | |
73 | } | |
74 | } | |
75 | ||
77d0155b | 76 | extension MapTextEditorController: NSTextStorageDelegate { |
fdb4633d | 77 | |
77d0155b RBR |
78 | override func textStorageDidProcessEditing(_ obj: Notification) { |
79 | if let textStorage = obj.object as? NSTextStorage { | |
fdb4633d | 80 | self.colorizeText(textStorage: textStorage) |
77d0155b RBR |
81 | } |
82 | } | |
83 | ||
84 | private func colorizeText(textStorage: NSTextStorage) { | |
85 | let range = NSMakeRange(0, textStorage.length) | |
86 | var matches = vertexRegex.matches(in: textStorage.string, options: [], range: range) | |
77d0155b RBR |
87 | |
88 | for match in matches { | |
fdb4633d RBR |
89 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1)) |
90 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 2)) | |
91 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 3)) | |
92 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 4)) | |
77d0155b RBR |
93 | } |
94 | ||
95 | matches = edgeRegex.matches(in: textStorage.string, options: [], range: range) | |
96 | ||
97 | for match in matches { | |
fdb4633d | 98 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 1)) |
77d0155b RBR |
99 | let arrowRange = match.range(at: 2) |
100 | textStorage.addAttributes( | |
fdb4633d | 101 | [.foregroundColor: NSColor.syntax.symbol], |
77d0155b | 102 | range: NSMakeRange(arrowRange.lowerBound - 1, arrowRange.length + 1)) |
fdb4633d | 103 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 3)) |
77d0155b RBR |
104 | } |
105 | ||
106 | matches = opportunityRegex.matches(in: textStorage.string, options: [], range: range) | |
107 | ||
108 | for match in matches { | |
fdb4633d RBR |
109 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) |
110 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2)) | |
111 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.symbol], range: match.range(at: 3)) | |
112 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 4)) | |
77d0155b RBR |
113 | } |
114 | ||
115 | matches = blockerRegex.matches(in: textStorage.string, options: [], range: range) | |
116 | ||
117 | for match in matches { | |
fdb4633d RBR |
118 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) |
119 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.vertex], range: match.range(at: 2)) | |
120 | } | |
121 | ||
122 | matches = noteRegex.matches(in: textStorage.string, options: [], range: range) | |
123 | ||
124 | for match in matches { | |
125 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) | |
126 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 2)) | |
127 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 3)) | |
77d0155b RBR |
128 | } |
129 | ||
130 | matches = stageRegex.matches(in: textStorage.string, options: [], range: range) | |
131 | ||
132 | for match in matches { | |
fdb4633d RBR |
133 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.option], range: match.range(at: 1)) |
134 | textStorage.addAttributes([.foregroundColor: NSColor.syntax.number], range: match.range(at: 2)) | |
77d0155b RBR |
135 | } |
136 | } | |
137 | } | |
138 | ||
5e8ff485 RBR |
139 | struct MapTextEditor: NSViewControllerRepresentable { |
140 | ||
141 | @Binding var text: String | |
fdb4633d | 142 | var onChange: () -> Void = {} |
5e8ff485 RBR |
143 | |
144 | func makeNSViewController( | |
145 | context: NSViewControllerRepresentableContext<MapTextEditor> | |
146 | ) -> MapTextEditorController { | |
fdb4633d | 147 | return MapTextEditorController(text: $text, onChange: onChange) |
5e8ff485 RBR |
148 | } |
149 | ||
150 | func updateNSViewController( | |
151 | _ nsViewController: MapTextEditorController, | |
152 | context: NSViewControllerRepresentableContext<MapTextEditor> | |
fdb4633d | 153 | ) {} |
5e8ff485 | 154 | } |